Skip to content

Honor per-file ServiceProviderSource override in the CLI codegen paths (2.3.0)#401

Merged
jeremydmiller merged 2 commits into
mainfrom
fix-2991-codegen-write-serviceprovider-source
May 31, 2026
Merged

Honor per-file ServiceProviderSource override in the CLI codegen paths (2.3.0)#401
jeremydmiller merged 2 commits into
mainfrom
fix-2991-codegen-write-serviceprovider-source

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes the root cause of wolverine#2991 / wolverine#2992: "HTTP endpoints resolve services wrong in codegen, but works in tests."

Root cause

The runtime type loader applies a per-ICodeFile service-provider override:

DynamicTypeLoader.Initialize / CompileAndAttach:
  if (serviceVariables != null && file.TryReplaceServiceProvider(out var sp))
      serviceVariables.ReplaceServiceProvider(sp);

So a Wolverine HTTP endpoint configured with ServiceProviderSource.FromHttpContextRequestServices resolves its service-located dependencies from httpContext.RequestServices.

The CLI codegen paths in DynamicCodeBuilder (WriteGeneratedCode / generateCode / TryBuildAndCompileAll) call generatedAssembly.GenerateCode(services) without that step. So dotnet run -- codegen write produced different and wrong code than the runtime: an opaque scoped lambda-factory dependency fell back to a created serviceScope, while a sibling DbContext used httpContext.RequestServices → two different scoped instances for one logical request.

Fix

Apply the override per file in all three CLI loops via a shared applyServiceProviderOverride(file, services).

The non-obvious part (regression guard)

The runtime path registers IServiceVariableSource as transient (fresh per file), so its ReplaceServiceProvider is naturally isolated. The CLI reuses one shared instance across every file, and ServiceCollectionServerVariableSource latches _replacedServiceProvider = true permanently (StartNewMethod only re-creates the default scope while it is false). So naively applying the override in the loop would leak httpContext.RequestServices into every subsequent file — non-HTTP message handlers, IsolatedAndScoped endpoints, etc.

The helper therefore calls a new IServiceVariableSource.ResetServiceProvider() (default no-op; implemented on ServiceCollectionServerVariableSource) before each file, so each file's override is isolated.

Tests

  • New DynamicCodeBuilderTests.per_file_service_provider_override_is_isolated_to_its_own_file: an HTTP-style file (opts into the override) followed by a plain file, asserting the call sequence reset, replace, reset — i.e. the plain file does not inherit the override.
  • All 399 CodegenTests pass on net9.0 + net10.0.

Verified downstream

Built WolverineWebApi (the #2992 repro) against this (local 2.2.8) and ran codegen write: the repro endpoint now resolves the opaque scoped service and the DbContext both from httpContext.RequestServices, and no non-HTTP MessageHandler file references httpContext.RequestServices (reset isolates correctly).

Bumps JasperFxVersion to 2.2.8.

Note: this changes codegen write/preview/test output for any consumer using a ServiceProviderSource override, so the downstream Wolverine PR (pin bump + de-skipping the #2992 repro) should run the full Wolverine suite to confirm no regressions.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits May 31, 2026 07:31
…s (wolverine GH-2991)

The runtime type loader (DynamicTypeLoader.Initialize/CompileAndAttach) applies a
per-ICodeFile service-provider override via
file.TryReplaceServiceProvider() -> IServiceVariableSource.ReplaceServiceProvider(),
so e.g. a Wolverine HTTP endpoint configured with
ServiceProviderSource.FromHttpContextRequestServices resolves its service-located
dependencies from httpContext.RequestServices. The CLI codegen paths in
DynamicCodeBuilder (write / preview / test) never applied that step, so
`dotnet run -- codegen write` generated different (and wrong) code than the runtime:
an opaque scoped lambda-factory dependency fell back to a created serviceScope while
a sibling DbContext used httpContext.RequestServices — yielding two different scoped
instances for one logical request.

Fix: apply the override per file in WriteGeneratedCode / generateCode /
TryBuildAndCompileAll via a shared applyServiceProviderOverride() helper.

Regression guard: unlike the runtime path (transient IServiceVariableSource, fresh
per file), the CLI reuses ONE shared source across all files, and ReplaceServiceProvider
latches permanently (StartNewMethod only re-creates the default scope when it has not
been replaced). So the helper first calls a new IServiceVariableSource.ResetServiceProvider()
to undo any prior file's override — otherwise httpContext.RequestServices would leak into
every following file (non-HTTP handlers, IsolatedAndScoped endpoints). New
DynamicCodeBuilder test asserts the per-file reset+replace sequence isolates the override.

Bumps to 2.2.8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller changed the title Honor per-file ServiceProviderSource override in the CLI codegen paths (2.2.8) Honor per-file ServiceProviderSource override in the CLI codegen paths (2.3.0) May 31, 2026
@jeremydmiller jeremydmiller merged commit 677fdf0 into main May 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant